﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Text.RegularExpressions;
using System.Net.NetworkInformation;
using System.Threading;
using System.Threading.Tasks;

namespace SMTP.Test.Client.Requests
{
    public class PingResponse : EventArgs
    {
        public string Response { get; private set; }

        public PingResponse(string msg)
        {
            Response = msg;
        }
    }

    class PingTest
    {
        //events to be relayed back to main thread
        public event EventHandler<PingResponse> PingResponseMessage;
        public event EventHandler PingComplete;

        //default vaules for attempts & timeouts
        const int DEFAULT_TIMEOUT = 3000;
        const int DEFAULT_PING_ATTEMPTS = 4;
        
        // packets sent/received/lost data
        int PacketsSent = 0;
        int PacketsReceived = 0;
        int PacketsLost = 0;
        double PercentPacketsLost = 0; 

        //round trip data
        long MaxRoundTrip = 0;
        long MinRoundTrip = long.MaxValue; //set to highest value so first repsonse automatically is registerd 
        double AvgRoundTrip = 0; 
        long TotalRoundTrip = 0;

        //server data
        string Host = string.Empty;
        string IP = string.Empty;

        //ping count
        int MaxPingCount;
        
        //cancel pinging task
        CancellationTokenSource SourceToken;
        
        public PingTest(string host, int count = DEFAULT_PING_ATTEMPTS)
        {
            Host = host;
            if (count <= 0)
            {
                //not a true ping forever, but whose gonna ping a host over 2 million times???
                MaxPingCount = Int32.MaxValue;
            }
            else
            {
                MaxPingCount = count;
            }

            TaskScheduler.UnobservedTaskException += new EventHandler<UnobservedTaskExceptionEventArgs>(TaskScheduler_UnobservedTaskException);
        }

        void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
        {
            e.SetObserved();            
        }

        public void PingHost()
        {
            SourceToken = new CancellationTokenSource();

            Task taskPing = new Task(() =>
            {
                bool Cancelled = false;

                try
                {
                    ResolveHost(Host);
                    if (!string.IsNullOrEmpty(IP))
                    {
                        Ping pingSender = new Ping();
                        PingOptions options = new PingOptions();

                        // Use the default Ttl value which is 128,
                        // but change the fragmentation behavior.
                        options.DontFragment = true;

                        // Create a buffer of 32 bytes of data to be transmitted.
                        string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
                        byte[] buffer = Encoding.ASCII.GetBytes(data);
                        int timeout = DEFAULT_TIMEOUT;

                        OnPingResponseMessage("Pinging " + Host + " [" + IP + "] with " + buffer.Length.ToString() + " bytes of data:");
                        for (int i = 1; i <= MaxPingCount; i++)
                        {
                            if (!SourceToken.Token.IsCancellationRequested)
                            {
                                PingReply reply = pingSender.Send(Host, timeout, buffer, options);
                                PacketsSent = i;
                                if (reply.Status == IPStatus.Success)
                                {
                                    //update packets received count
                                    PacketsReceived += 1;

                                    //generate message and relay back to main
                                    string msg = "Reply from " + reply.Address.ToString() + ": " + "bytes=" + reply.Buffer.Length.ToString() + " time=" + reply.RoundtripTime + "ms TTL=" + reply.Options.Ttl;
                                    OnPingResponseMessage(msg);

                                    //set max round trip
                                    if (MaxRoundTrip < reply.RoundtripTime)
                                        MaxRoundTrip = reply.RoundtripTime;

                                    //set min round trip
                                    if (MinRoundTrip > reply.RoundtripTime)
                                        MinRoundTrip = reply.RoundtripTime;

                                    //total round trip
                                    TotalRoundTrip += reply.RoundtripTime;

                                    //simulate smooth output like DOS
                                    System.Threading.Thread.Sleep(750);
                                }
                                else
                                {
                                    PacketsLost += 1;
                                    if (reply.Status == IPStatus.TimedOut)
                                        OnPingResponseMessage("Request timed out");
                                    else
                                        OnPingResponseMessage("Request: " + reply.Status.ToString());
                                }
                            }
                            else
                            {
                                Cancelled = true;
                                SourceToken.Token.ThrowIfCancellationRequested();
                            }
                        }


                        if (!Cancelled)
                        {
                            //send first message back
                            OnPingResponseMessage("Ping statistics for " + IP + ":");

                            //get % of packets lost
                            PercentPacketsLost = PacketsReceived / Convert.ToDouble(PacketsSent) * 100;
                            PercentPacketsLost = 100.00 - PercentPacketsLost;

                            //send packet data back to GUI
                            OnPingResponseMessage("Packets: Sent = " + PacketsSent.ToString() + ", Received = " + PacketsReceived.ToString() + ", Lost = " + PacketsLost.ToString() + " (" + PercentPacketsLost.ToString() + "% loss)");

                            //if we didn't loose everything relay min, max & average round trip
                            if (PacketsSent > PacketsLost)
                            {
                                //send inital response to GUI
                                OnPingResponseMessage("Approximate round trip times in milli-seconds:");

                                //get average
                                AvgRoundTrip = Math.Round(Convert.ToDouble(TotalRoundTrip) / Convert.ToDouble(PacketsReceived));

                                //relay final ttl stats back to GUI
                                OnPingResponseMessage("Minimum = " + MinRoundTrip.ToString() + "ms, Maximum = " + MaxRoundTrip.ToString() + "ms, Average = " + AvgRoundTrip.ToString() + "ms");
                            }
                        }
                    }
                    else
                    {
                        OnPingResponseMessage("Unable to resolve host address for " + Host);
                    }

                }
                catch (Exception ex)
                {
                    OnPingResponseMessage(ex.Message);
                }

                OnPingComplete();

            }, SourceToken.Token);

            taskPing.Start();
        }

        public void Cancel()
        {
            if (SourceToken.Token.CanBeCanceled) SourceToken.Cancel();
        }

        private void ResolveHost(string host)
        {
            try
            {
                //resolve host
                IPHostEntry hostip = host.GetIPHostEntry();
                if ((hostip != null) && (hostip.AddressList.Count() > 0))
                {
                    Host = hostip.HostName;
                    IP = hostip.AddressList[0].ToString();
                }
            }
            catch (Exception)
            {
                throw new Exception("Ping request unable to resolve host " + host);
            }
        }

        private void OnPingResponseMessage(string msg)
        {
            var handler = PingResponseMessage;
            if (handler != null) PingResponseMessage(this, new PingResponse(msg));
        }

        private void OnPingComplete()
        {
            var handler = PingComplete;
            if (handler != null) PingComplete(this, EventArgs.Empty);
        }
       
    }
}
